package flexo.graphic;

import java.nio.FloatBuffer;

import org.lwjgl.opengl.*;

import flexo.math.*;
import flexo.scene.Node;

public class Camera implements Comparable<Camera>
{
	public enum ProjectionType
	{
		ORTHOGONAL,
		PERSPECTIVE
	}
	
	protected ProjectionType projType;
	protected static Camera renderingCamera;
	private int layer;	
	private Color backgroundColor;
	protected boolean depthTest = true;
	
	protected Node posNode;
	protected Node targetNode;
	
	protected Vector position;
	protected Vector target;
	protected Vector up;
	protected int[] viewport;
	protected boolean scissors;
	
	protected Matrix viewMatrix;
	protected Matrix projectionMatrix;	
	
	protected boolean updateViewMatrix;
	protected boolean updateProjectionMatrix;
	
	protected RenderQueue renderQueue;
	
	protected float fov = 70.0f;
	
	Camera(String scene)
	{
		init(scene,ProjectionType.PERSPECTIVE);
	}
	
	Camera(String scene, ProjectionType type)
	{
		init(scene,type);
	}
	
	protected void init(String scene, ProjectionType type)
	{
		projType = type;
		flexo.Kernel.log("flexo.graphic.Camera", "Creating camera");
		viewport = new int[4];
		// We set up the viewport to cover all the screen
		setViewport(0,0,1,1);
		backgroundColor = new Color();
		position = new Vector(0,0,0);
		target = new Vector(0,0,-1);
		up = new Vector(0,1,0);	
		viewMatrix = new Matrix();
		projectionMatrix = new Matrix();
		updateViewMatrix = true;
		updateProjectionMatrix = true;
		
		flexo.scene.SceneSystem ss = (flexo.scene.SceneSystem)flexo.Kernel.getSystem(flexo.scene.SceneSystem.ID);
		renderQueue = new RenderQueue(ss.getScene(scene),this);
	}
	
	public void setPositionNode(Node n)
	{
		posNode = n;
	}
	
	public void setTargetNode(Node n)
	{
		targetNode = n;
	}
	
	public float getFOV()
	{
		return fov;
	}
	
	public void setFOV(float fov)
	{
		if (fov > 0.0f)
		{
			this.fov = fov;
			this.updateProjectionMatrix = true;
		}
	}	
	
	public static Camera getRenderingCamera()
	{
		return renderingCamera;
	}
	
	public Matrix getViewMatrix()
	{
		GL11.glMatrixMode(GL11.GL_MODELVIEW);
		
		if (updateViewMatrix)
		{
			updateViewMatrix();
			updateViewMatrix = false;
		}
		return viewMatrix;
	}
	
	public Matrix getProjectionMatrix()
	{		
		GL11.glMatrixMode(GL11.GL_PROJECTION);
		
		if (updateProjectionMatrix)
		{
			updateProjectionMatrix();
			updateProjectionMatrix = false;
		}
		return projectionMatrix;
	}
	
	/**
	 * Removes the camera from the engine's camera list.
	 * Remember that if this is the last camera in the engine, it won't be deleted.
	 */ 
	public void remove()
	{
		flexo.Kernel.log("flexo.graphic.Camera", "Removing camera");
		renderQueue.remove();
		flexo.graphic.GraphicSystem gs = (flexo.graphic.GraphicSystem)flexo.Kernel.getSystem(flexo.graphic.GraphicSystem.ID);
		gs.removeCamera(this.layer);
	}
	
	public void setViewport(float bottomLeft, float topRight)
	{
		setViewport(bottomLeft,bottomLeft,topRight,topRight);
	}
	
	/**
	 * Sets the Viewport position (the portion of the screen where the scene is rendered)
	 * All values range from 0 to 1 and represent the percentage of screen, not fixed size.
	 */
	public void setViewport(float bottomLeftX, float bottomLeftY, float topRightX, float topRightY)
	{
		if (bottomLeftX >= 0 && bottomLeftY >= 0 && topRightX >= 0 && topRightY >= 0
				&& bottomLeftX <= 1 && bottomLeftY <= 1 && topRightX <= 1 && topRightY <= 1
				&& bottomLeftX < topRightX && bottomLeftY < topRightY)
		{
			DisplayMode current = Display.getDisplayMode();
			if (bottomLeftX == 0 && bottomLeftY == 0 && topRightX == 1 && topRightY == 1)
			{
				scissors = false;
			}
			else
			{
				scissors = true;
			}
			viewport[0] = (int)(bottomLeftX * current.getWidth());
			viewport[2] = (int)( (topRightX-bottomLeftX) * current.getWidth());
			viewport[1] = (int)(bottomLeftY * current.getHeight());
			viewport[3] = (int)( (topRightY-bottomLeftY) * current.getHeight());
		}			
	}

	public void setBackgroundColor(Color backgroundColor)
	{
		this.backgroundColor = backgroundColor;
	}
	
	void update()
	{
		if (posNode != null && !position.equals(posNode.getAbsolutePosition()))
		{
			setPosition(posNode.getAbsolutePosition());
		}
		if (targetNode != null && !target.equals(targetNode.getAbsolutePosition()))
		{
			setTarget(targetNode.getAbsolutePosition());
		}		
		
		// Set the viewport
		GL11.glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
		if (scissors)
		{			
			GL11.glEnable(GL11.GL_SCISSOR_TEST);
			GL11.glScissor(viewport[0], viewport[1], viewport[2], viewport[3]);
		}
		else
		{
			GL11.glDisable(GL11.GL_SCISSOR_TEST);
		}
		
		// Enables Depth Testing
		if (depthTest)
		{
			GL11.glEnable(GL11.GL_DEPTH_TEST);
			GL11.glDepthFunc(GL11.GL_LEQUAL);
		}
		else
		{
			GL11.glDisable(GL11.GL_DEPTH_TEST);
		}
		
		// If the background is transparent we clean only the Depth Buffer
		if (depthTest && backgroundColor.getAlpha() == 0.0f)
		{
			GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT);
		}
		else // Clear The Screen And The Depth Buffer
		{
			GL11.glClearColor(backgroundColor.getRed(),backgroundColor.getGreen(),backgroundColor.getBlue(),backgroundColor.getAlpha());
			if (depthTest)
			{
				GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
			}
			else
			{
				GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
			}
		}				
		
		// Update the projection matrix
		GL11.glMatrixMode(GL11.GL_PROJECTION);
		GL11.glLoadMatrix(getProjectionMatrix().getDataBuffer());			
		
		// Update the view matrix
		GL11.glMatrixMode(GL11.GL_MODELVIEW);
		GL11.glLoadMatrix(getViewMatrix().getDataBuffer());
		Camera.renderingCamera = this;
	}
	
	void render()
	{				
		renderQueue.render();
		/*
		GL11.glBegin( GL11.GL_TRIANGLE_STRIP );
		GL11.glVertex3f(0,0,0);
		GL11.glVertex3f(100,0,0);
		GL11.glVertex3f(0,100,0);
		GL11.glVertex3f(100,100,0);
		GL11.glEnd();		
		*/
	}
	
	public void setProjectionType(ProjectionType t)
	{
		this.projType = t;
	}
	
	protected void updateProjectionMatrix()
	{
		GL11.glLoadIdentity();
		flexo.graphic.GraphicSystem gs = (flexo.graphic.GraphicSystem)flexo.Kernel.getSystem(flexo.graphic.GraphicSystem.ID);
		Vector display = gs.getDisplayMode();
		
		if (projType == ProjectionType.ORTHOGONAL)
		{
			GL11.glOrtho(0, (float)display.x, 0, (float)display.y, -1, 1);
			GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, projectionMatrix.getDataBuffer());
		}
		else
		{
			float aspectRatio = display.x/display.y;
			gluPerspective(fov,aspectRatio,1.0f,1000.0f);			
		}		
	}
	
	private void gluPerspective(float fov, float aspect, float zNear, float zFar)
	{		
		float sine, cotangent, deltaZ;
		float radians = fov / 2 * ((float)Math.PI) / 180;

		deltaZ = zFar - zNear;
		sine = (float) Math.sin(radians);
		cotangent = (float) Math.cos(radians) / sine;

		projectionMatrix.reset();
		
		FloatBuffer buffer = projectionMatrix.getDataBuffer(); 

		buffer.put(0 * 4 + 0, cotangent / aspect);
		buffer.put(1 * 4 + 1, cotangent);
		buffer.put(2 * 4 + 2, - (zFar + zNear) / deltaZ);
		buffer.put(2 * 4 + 3, -1);
		buffer.put(3 * 4 + 2, -2 * zNear * zFar / deltaZ);
		buffer.put(3 * 4 + 3, 0);
	}
	
	protected void updateViewMatrix()
	{
		// gluLookAt
		FloatBuffer buffer = viewMatrix.getDataBuffer();
		buffer.rewind();
				
		Vector fwd = Vector.getTempVector();		
		Vector.sub(target, position, fwd);
		fwd.normalize();
		
		Vector right = getRightVector();
		Vector realUp = Vector.getTempVector();
		Vector.cross(right, fwd, realUp);
		
		buffer.put(right.x);
		buffer.put(realUp.x);
		buffer.put(-fwd.x);
		buffer.put(0);
		
		buffer.put(right.y);
		buffer.put(realUp.y);
		buffer.put(-fwd.y);
		buffer.put(0);
		
		buffer.put(right.z);
		buffer.put(realUp.z);
		buffer.put(-fwd.z);
		buffer.put(0);
		
		buffer.put(-(right.x*position.x + right.y*position.y + right.z*position.z));
		buffer.put(-(realUp.x*position.x + realUp.y*position.y + realUp.z*position.z));
		buffer.put(fwd.x*position.x + fwd.y*position.y + fwd.z*position.z);
		buffer.put(1);
	}

	public int getLayer()
	{
		return layer;
	}

	public void setLayer(int layer)
	{
		this.layer = layer;
		flexo.graphic.GraphicSystem gs = (flexo.graphic.GraphicSystem)flexo.Kernel.getSystem(flexo.graphic.GraphicSystem.ID);
		gs.sortCameraList = true;
	}
	
	public int compareTo(Camera cam)
	{
		return this.layer - cam.layer;
	}

	public Vector getPosition()
	{
		return position;
	}
	
	public Vector getTarget()
	{
		return target;
	}
	
	public Vector getUpVector()
	{
		return up;
	}
	
	public Vector getForwardVector()
	{
		Vector f = Vector.getTempVector();
		Vector.sub(target, position, f);
		f.normalize();
		return f;
	}
	
	public Vector getRightVector()
	{
		Vector r = Vector.getTempVector();
		Vector.cross(getForwardVector(), getUpVector(), r);
		r.normalize();
		return r;
	}
	
	protected void setPosition(Vector p)
	{
		position.set(p);
		this.updateViewMatrix = true;
	}
	
	public void move(Vector m)
	{
		position.move(m);
		this.updateViewMatrix = true;
	}
	
	protected void setTarget(Vector t)
	{
		target.set(t);
		this.updateViewMatrix = true;
	}
	
	public void setUpVector(Vector u)
	{		
		up.set(u);
		up.normalize();
		this.updateViewMatrix = true;
	}
	
	public String toString()
	{
		return "Layer: " + layer;
	}
}